plus macOS Guide
Build Hub
Implementation guide

dot+
for macOS

The iOS app is the source of truth. macOS is a menu-bar companion that reuses the same design system and services and re-homes the UI for the desktop: a status-bar item, a detachable popover for capture and review, and an optional full window for the library. This page is what you build against.

Entry point
MenuBarExtra
Min target
macOS 14 Sonoma
Framework
SwiftUI
Design
dot+ Base v1.1
Reuse before you rebuild. Tokens, brand, and the service layer are cross-platform Swift — carry them over untouched. The work is re-homing the views from touch to pointer, and swapping the few UIKit calls (haptics, UIColor, share sheet) for AppKit equivalents. The map below marks exactly what reuses, adapts, or is new.
01

The interactive reference

The clickable mockup below is the behavioural spec. Every menu-bar state is modelled — see it work, then build it. Use the Tweaks panel (top-right of the frame) to jump between states and try theme, accent, panel width, and layout variants.

dot+ for macOS — menu-bar app Open full screen
Also see macOS Variations for alternate popover and window structures under consideration.
02

Platform translation

How each iOS pattern re-homes on the desktop. The design language does not change — the container and the input model do. Touch becomes pointer; the tab bar becomes structure; the Dynamic Island becomes the status item.

iOS patternmacOS equivalent
Bottom tab barDotTabBar · dot+ ext
Popover = single inline view; full window uses a leading list / NavigationSplitView. The 4 tabs become a segmented control or sidebar.
Bottom sheets.sheet / detents
In the popover, present inline (push). In the window, use a centered .sheet sized to content — no detents on macOS.
Dynamic Island + notificationsrecording status
Menu-bar status item shows the live timer + red disc; meeting-detected uses UNUserNotification in Notification Center.
Haptics.light / .medium / .success
No system haptics on Mac. Drop them, or use NSHapticFeedbackManager (trackpad only, sparingly). Keep the visual state change.
Navigation pushNavigationStack
Same NavigationStack works. In the popover keep depth shallow — prefer a back affordance over deep stacks.
Swipe actions / pull-to-refreshlist gestures
Hover-reveal buttons + right-click .contextMenu. Refresh is a toolbar button, not a pull.
Share sheetUIActivityViewController
ShareLink or NSSharingServicePicker. Export to file uses .fileExporter.
Safe-area insets47pt status · 34pt home
None. Popover is chrome-less; window uses a hidden title bar (.windowStyle(.hiddenTitleBar)) with in-content controls.
03

Design tokens

Copy-paste ready. The only macOS change from the iOS token file: UIColor(dynamicProvider:) becomes NSColor(name:dynamicProvider:). Values are identical — tokens.json is the law. Never reference primitives from views; always go through semantic.

Semantic color — SwiftUI (macOS) & CSS
SwiftDotColors+macOS.swift
import SwiftUI

// Theme-adaptive color from explicit dark/light primitives.
// macOS: wrap NSColor(name:dynamicProvider:) so SwiftUI
// updates with the effective appearance.
extension Color {
  static func themed(dark: Color, light: Color) -> Color {
    Color(NSColor(name: nil) { appearance in
      let isDark = appearance.bestMatch(
        from: [.aqua, .darkAqua]) == .darkAqua
      return isDark ? NSColor(dark) : NSColor(light)
    })
  }

  // Surfaces
  static let surfacePrimary   = themed(dark: .mono900, light: .mono0)
  static let surfaceSecondary = themed(dark: .mono800, light: .mono50)
  static let surfaceTertiary  = themed(dark: .mono700, light: .mono100)
  static let surfaceInverse   = themed(dark: .mono50,  light: .mono1000)
  // Content
  static let contentPrimary   = themed(dark: .mono0,   light: .mono1000)
  static let contentSecondary = themed(dark: .mono300, light: .mono600)
  static let contentTertiary  = themed(dark: .mono500, light: .mono500)
  // Border
  static let borderOpaque = themed(dark: .mono600, light: .mono200)
  static let borderSubtle = themed(dark: .mono700, light: .mono100)
  // State (theme-invariant)
  static let statePositive = Color(hex: 0x05944F)
  static let stateWarning  = Color(hex: 0xBB7A00)
  static let stateNegative = Color(hex: 0xE11900)
  static let stateInfo     = Color(hex: 0x276EF1)
}
CSStokens.css
/* primitive — mono */
:root {
  --mono-0:#FFFFFF; --mono-50:#FAFAFC;
  --mono-100:#F2F2F5; --mono-200:#E6E6EB;
  --mono-300:#C8C8D4; --mono-400:#9494A3;
  --mono-500:#6B6B78; --mono-600:#2E2E38;
  --mono-700:#222229; --mono-800:#18181F;
  --mono-900:#0F0F14; --mono-1000:#05050A;
  /* accents */
  --positive:#05944F; --warning:#BB7A00;
  --negative:#E11900; --info:#276EF1;
  /* semantic — dark (primary) */
  --bg:var(--mono-900);  --bg-2:var(--mono-800);
  --bg-3:var(--mono-700); --bg-4:var(--mono-600);
  --fg:var(--mono-0);    --fg-2:var(--mono-300);
  --fg-3:var(--mono-400); --line:var(--mono-700);
}
/* light — one-swap override */
:root[data-theme=light] {
  --bg:var(--mono-0);   --bg-2:var(--mono-50);
  --bg-3:var(--mono-100); --fg:var(--mono-1000);
  --fg-2:var(--mono-600); --line:var(--mono-100);
}
Spacing · radius · type — the 4pt grid
SwiftDotSpacing.swift
enum DotSpacing {           // 4pt grid
  static let s1: CGFloat = 4   // icon-label gap
  static let s2: CGFloat = 8   // within-component
  static let s3: CGFloat = 12  // card / button pad
  static let s4: CGFloat = 16  // edge, between cards
  static let s5: CGFloat = 20  // section spacing
  static let s6: CGFloat = 24  // major break
  static let s7: CGFloat = 32  // above headers
  static let s8: CGFloat = 48
}
enum DotRadius {            // never > 16
  static let xs: CGFloat = 4   // tags, badges
  static let sm: CGFloat = 8   // cards, buttons
  static let md: CGFloat = 12
  static let lg: CGFloat = 16  // ceiling
}
SwiftDotTypography.swift
extension Font {  // Inter + JetBrains Mono
  static let dotDisplay    = custom("Inter-Bold",     size: 32)
  static let dotHeadline   = custom("Inter-Bold",     size: 22)
  static let dotTitle      = custom("Inter-SemiBold", size: 18)
  static let dotBody       = custom("Inter-Regular",  size: 15)
  static let dotBodyStrong = custom("Inter-SemiBold", size: 15)
  static let dotLabel      = custom("Inter-Medium",   size: 13)
  static let dotCaption    = custom("JetBrainsMono-Regular", size: 11)
}
// Register fonts in the app target's Info.plist
// under ATSApplicationFontsPath (macOS) — not UIAppFonts.
macOS font gotcha. On iOS custom fonts register via UIAppFonts. On macOS use ATSApplicationFontsPath in Info.plist (point it at a Fonts/ folder in the bundle), or register at launch with CTFontManagerRegisterFontsForURL. If a face is missing, the tokens fall back to SF Pro / SF Mono automatically.
04

Components → SwiftUI

The core components are cross-platform Swift. They render as-is; the only edits are pointer affordances (hover, focus ring) and removing the UIKit haptic call. Each card: the live look, the contract, and the macOS note.

DotButtonBase
4 variants × 4 sizes (28/36/48/56) × 4 shapes. Primary = inverse surface; secondary = tinted, no border; tertiary = 1pt border; destructive = negative fill.
macOS: drop UIImpactFeedbackGenerator. Add .onHover brightness lift + .focusable() ring. Keep the 0.97 press scale.
DotTagBase
default done 3 todos error Gemini 2.5
24pt height, radius.xs, caption type. Semantic variants use a 16% tinted fill of the state color, no border.
macOS: renders identically — no changes.
DotToggleBase
42×24 track, 20×20 knob. On = inverse-surface track; off = opaque border. 0.15s easeInOut, no spring.
macOS: keep the custom toggle for brand consistency — do not fall back to the native Switch style.
DotInputBase
Meeting title Design Review
48pt height, tertiary fill, transparent border → info on focus, negative on error. Label above, caption below.
macOS: the focus border is also the keyboard focus ring — wire it to @FocusState, not just tap.
DotButton — usageBase
SwiftSame call site, iOS & macOS
DotButton("Record Meeting", icon: "mic.fill") { start() }
DotButton("Upload Audio", variant: .secondary) { pick() }
DotButton("Cancel", variant: .tertiary, size: .small) { dismiss() }
DotButton("", icon: "mic.fill", shape: .circle) { start() }
The public API is unchanged across platforms — gate the one UIKit line (UIImpactFeedbackGenerator) behind #if os(iOS) inside the component. Every call site stays the same.
Full contracts for all seven components live in the Living Style Guide and tokens.json → components. Never add a color that isn't a token; never exceed radius 16; never use a spring; never use a shadow in dark mode (use surface tonal lift).
05

Screen build checklist

Every surface the macOS app needs, its iOS source, and the porting note. All are designed in the mockup and not yet built in the macOS target — check them off as you land each one.

ScreenmacOS noteStatus
Menu-bar status item
dotWidget · new
Idle = monochrome mark. Recording = red disc + live mm:ss timer. Click toggles the popover.
To build
Onboarding / Auth
Features/Onboarding
Sign-in in a centered window sheet, not full-screen. AuthenticationManager reused.
To build
Home / idle popover
Features/Home
Recent recordings + primary Record CTA. The popover's default state.
To build
Recording overlay
Features/Recording
Live meter/waveform + timer in the popover; status item mirrors the timer. Detachable, draggable.
To build
Processing
ProcessingTaskManager
1pt border spinner, no skeletons. Auto-advances to detail on completion.
To build
Recording detail
Recording/Views · 4 tabs
Overview · Transcript · Action Items · Details, plus mini player. Tabs → segmented control.
To build
Ask dot+ (AI)
Features/AI
Chat + voice canvas. Long-press → right-click / dedicated button. VoiceChatServices reused.
To build
Template Store
Features/TemplateStore
Best in the full window (grid). Recipe cards + detail sheet. Reflow from 1-col to grid.
Needs pass
Settings
Features/Settings
macOS convention: Settings scene (⌘,) as a tabbed window, plus quick toggles in the popover.
To build
Full app window
Home/AllRecordingsView
The library. NavigationSplitView — sidebar list, detail on the trailing side. Resizable.
To build
Meeting-detected notification
CalendarManager · Notifications
Notification Center banner with Record / Ignore actions via UNNotificationAction.
To build
Paywall
Features/Monetization
StoreKitService reused. Present as a window sheet. Verify macOS IAP entitlement.
Needs pass
06

Architecture map

The Xcode project, grouped. The service and token layers are platform-agnostic Swift — carry them over. The view layer is where the work is. Tags mark what reuses as-is, what needs a macOS adaptation, and what's net-new.

Reuse cross-platform, use as-is
Adapt re-home / swap UIKit calls
New macOS-specific

DesignSystem

dot+/DesignSystem
Tokens/ReuseColors, spacing, type, radius
Components/AdaptDrop haptics; add hover + focus
Brand/ReuseBrandMark, BrandMorphView

Services

dot+/Services · 26 files
RecordingManagerAdaptAVFoundation mic + macOS permission
AppleSpeechServiceAdaptSpeech entitlement on macOS
CloudGeminiServiceReuseNetworking, model calls
TranscriptionOrchestratorReusePipeline coordination
Diarization / Prompt / ExportReusePure logic + rendering
MeetingDocumentRendererReuseHTML output generator
StoreKitServiceAdaptmacOS IAP entitlement

Features (views)

dot+/Features
HomeAdaptPopover + split-view window
RecordingAdaptOverlay → popover, detach
AIAdaptLong-press → pointer
TemplateStoreAdaptGrid reflow in window
SettingsAdaptSettings scene (⌘,)
ShareAdaptShareLink / NSSharingService
Onboarding · Monetization · ContactsAdaptSheet-based re-home

Managers · App shell

dot+/Managers · dot_App.swift
MenuBarExtra sceneNewStatus item + popover host
Window managementNewDetach, full window, Settings
CalendarManagerReuseEventKit — meeting detection
NotificationsManagerAdaptUNUserNotification actions
Auth · Keychain · IdentityReuseCross-platform
Sequence. ① Compile the token + service layer into the macOS target (most builds unchanged). ② Stand up the MenuBarExtra shell + status item. ③ Port Home → Recording → Detail (the core loop). ④ Full window + Template Store grid. ⑤ Settings scene, notifications, paywall. Land the core capture loop before the library.